Skip to content

fix(db): align join keys with equality identity - #1834

Merged
KyleAMathews merged 3 commits into
mainfrom
codex/wave1-query-equality
Sep 17, 2026
Merged

KyleAMathews merged 3 commits into
mainfrom
codex/wave1-query-equality

Conversation

@KyleAMathews

@KyleAMathews KyleAMathews commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

Align live-query join keys with the query engine's existing equality identity. Joins now agree with eq for binary, Date, Temporal, scalar, BigInt/non-finite, and opaque-reference values; nullish operands are unmatched across direct, correlated, eager, and lazy joins.

Root cause

The lazy-demand optimizer reused an already-normalized join key as the backend query operand. For binary values, the collection index then normalized that internal string as a user string, so double normalization prevented the candidate from matching. The graph needs the equality identity as its key while lazy demand retains the raw operand. Separately, full and correlated joins allowed nullish operands from opposite sides to share a bucket.

Approach

  • Key non-nullish join operands through the existing graph-local ValueIdentity exactly once.
  • Give nullish operands compact side-local keys so opposite sides cannot match while same-side rows share one efficient bucket.
  • Retain the raw operand beside the graph key for lazy subset demand, with one stable representative and deterministic reference weights per equality class.
  • Remove the compiler's redundant post-join filter; db-ivm already guarantees the selected inner/left/right/full result shape, so the result mapper can be a direct pipe operator.
  • Keep the repair inside the query compiler; db-ivm, public APIs, exports, dependencies, and compatibility branches are unchanged.

Key invariants

  • A direct join, eq, and a filtered cross join agree over every established equality domain.
  • Equal binary copies match; unequal bytes and normalization-like strings remain disjoint.
  • null and undefined never match in joins, including when row keys overlap across sides.
  • Lazy demand receives original values, retires keys deterministically, and does no duplicate candidate work.
  • Route/correlation identity remains part of routed join keys.
  • The four supported join modes preserve the output shapes owned by db-ivm without a duplicate compiler filter node.

Non-goals

Trade-offs and package weight

Failing fast for established Date/Temporal/binary/reference equality would narrow existing query behavior, so the repair reuses the current identity relation instead. The emitted machinery is limited to a private getJoinKey helper and one function-scoped demand-weight map; the map keeps one raw representative per live equality class while balancing inserts and retractions. JoinInputValue is type-only.

The review pass replaced per-row nullish serialization with two inline side sentinels, removed an unused helper argument, and deleted a redundant post-join filter/closure. Recomputing the raw operand instead of retaining the tuple's third slot was rejected because it repeats expression evaluation and increased minified gzip by another 43 B ESM / 21 B CJS in the measured variant. Folding key policy into call sites and sharing unrelated demand bookkeeping were rejected because they reduce locality or broaden the change after the zero-growth target is met. Production TypeScript is net +1 line.

Against exact base 3ad64a42a0088e1272176fb33c953526fed9b868, every requested shipped-size measurement is now negative (85 modules per format, deterministic /usr/bin/gzip -n -9, Brotli q11/text):

Build Format Raw gzip Brotli
normal ESM -308 B -57 B -37 B
normal CJS -326 B -57 B -47 B
minified ESM -63 B -46 B -27 B
minified CJS -107 B -27 B -22 B

The complete normal/minified dist trees shrink by 1,392 B / 766 B. The 641-entry npm package shrinks by 124 B packed / 1,616 B unpacked after a normal build and 78 B packed / 990 B unpacked after a minified build. There are no new exported state fields, public APIs, exports, dependencies, compatibility branches, package metadata changes, or db-ivm production changes.

Verification

  • Exact base 3ad64a42a0088e1272176fb33c953526fed9b868: the 30-cell focused owner selection is RED (16 failed, 14 path/control passes) across binary, Temporal, nullish, lifecycle, and raw lazy-demand paths.
  • This branch: the same 30-cell selection is GREEN (30 passed), with positive execution in both autoIndex: off and eager modes.
  • The historical combined implementation path is killed by the RED baseline. A missing-nullish-side mutant is independently killed (4 failed), and an illegal upstream inner-join shape mutant is killed by five direct db-ivm owner tests.
  • Full join owner: 126/126 passed.
  • Final cold-reconciliation/subquery/scheduler collateral: 119/119; direct db-ivm join owners: 60/60. Earlier equality/lifecycle collateral passed 229/229; hash/value controls passed 90/90; join/index boundary passed 71/71.
  • Full DB runtime gate with two workers and type-only files excluded: 178 files / 5,930 tests passed.
  • Focused join type specs: 36/36 passed; the standalone invocation separately reports the repository's known 35 cross-package rootDir diagnostics. Normal and minified @tanstack/db builds, including declaration generation, passed.
  • Changed-file ESLint, Prettier, git diff --check, and the commit hook passed.

Key rerun commands:

../../node_modules/.bin/vitest run tests/query/join.test.ts --maxWorkers=1 --minWorkers=1
../../node_modules/.bin/vitest run tests/query/join-subquery.test.ts tests/query/cold-join-reconciliation-oracle.test.ts tests/query/scheduler.test.ts --maxWorkers=1 --minWorkers=1
../../node_modules/.bin/vitest run --typecheck.enabled=false --maxWorkers=2 --minWorkers=1
../../node_modules/.bin/vite build
../../node_modules/.bin/vite build --minify

Files changed

  • packages/db/src/query/compiler/joins.ts: unify join keying with ValueIdentity, preserve raw lazy-demand values, and remove redundant result filtering.
  • packages/db/tests/query/join.test.ts: permanent cross-formulation, lifecycle, disjointness, nullish, and lazy-work coverage.
  • .changeset/fix-query-join-equality.md: patch release note for @tanstack/db.

Provenance and credit

Source Contributor(s) Reuse / learning
#593 / #861 Lucas Duailibe (@duailibe), Sam Willis (@samwillis), Vincent Chan (@lousydropout) Compound-demand evidence and join-first/where-second reference. #861 is prior art only; no code was reused and compound joins remain design-gated.
#896 Tomas Zaluckij (@Tomaszal) Equal-byte copies, unequal controls, and name-join/eq equivalence exposed the binary inconsistency.
#899 Kyle Mathews (@KyleAMathews), Claude (@claude), Sam Willis (@samwillis) The proposed Map-reference diagnosis was disproved, while the binary test idea, shared-matrix constraint, and common-path cost concern informed this repair. The global normalization/string-prefix approach is superseded.
#1258 Kyle Mathews, Claude, Kevin De Porre (@kevin-dp) Rejected reference-equality policy and allocation concerns informed negative policy and work controls.
#1229 Hieu Nguyen / Hiếu Nguyễn Minh (@hugiex), Kevin De Porre, Claude Opus 4.6, Sam Willis Date normalization and lazy-in lineage; this PR retains raw demand values while using the current identity once.
#1797 Kyle Mathews Introduced the normative ValueIdentity relation used here.
#1822 Kyle Mathews Landed the required hashing/equality foundation and is the exact refreshed base.

Addresses the binary join inconsistency reported in #896 and credits/supersedes the approach in #899. No closure keywords are used because #896 is already closed, the other referenced records are pull requests or partial/design-gated work, and no material prior implementation code was copied (so no co-author trailers are warranted).

Canonicalize satisfiable join operands through the compilation ValueIdentity, keep nullish operands row-disjoint, and preserve raw lazy-demand values with stable representatives. This restores equality-predicate consistency for binary and other established value domains without adding compound join syntax.

Provenance: #593 Lucas Duailibe and Sam Willis; #861 Vincent Chan; #896 Tomas Zaluckij; #779/#899/#1258 Kyle Mathews and Claude; #899 review by Sam Willis; #1229 Hieu Nguyen, Hiếu Nguyễn Minh, Kevin De Porre, and Claude Opus 4.6; #1797 ValueIdentity architecture by Kyle Mathews. Prior art informed the evidence and design; no prior implementation lines were reused. #593/#861 remain design-gated.

Weight: production source net +30 lines; emitted join module +65 B ESM gzip and +60 B CJS gzip. No public API, export, dependency, compatibility branch, or db-ivm change.
@coderabbitai

coderabbitai Bot commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 45c69db5-4592-4b87-a2b4-ec47c2108208

📥 Commits

Reviewing files that changed from the base of the PR and between ba24eba and 5111a82.

📒 Files selected for processing (1)
  • packages/db/src/query/compiler/joins.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 4 remain after this review.


📝 Walkthrough

Walkthrough

The join compiler now preserves raw join values and uses predicate-compatible equality keys. Lazy loading skips nullish values and tracks raw demand values. Result processing no longer filters rows by join type. Tests cover equality, binary, temporal, opaque, nullish, lifecycle, and lazy-loading behavior.

Changes

Join Equality Alignment

Layer / File(s) Summary
Join key generation
packages/db/src/query/compiler/joins.ts
Adds JoinInputValue. getJoinKey uses side-specific sentinels for nullish values and serializes non-routed equality values.
Join pipeline and result processing
packages/db/src/query/compiler/joins.ts
Both join pipelines preserve raw join values. Lazy demand tracking skips nullish values and indexes generated keys. Result processing merges rows and builds composite keys without join-type filtering.
Equality and lazy-loading regression coverage
packages/db/tests/query/join.test.ts, .changeset/fix-query-join-equality.md
Tests compare joins with equality predicates across value types, cover nullish and binary transitions, validate eager and non-eager lazy loading, and record a patch release.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant JoinPipeline
  participant getJoinKey
  participant LazyDemandTracking
  participant CollectionLoader
  JoinPipeline->>getJoinKey: derive key from raw join value
  getJoinKey-->>LazyDemandTracking: provide generated join key
  LazyDemandTracking->>CollectionLoader: issue IN-predicate load for non-nullish values
  CollectionLoader-->>LazyDemandTracking: return candidate records
Loading

Merge Risk: ⚪ Minimal · up to 5111a

The join equality alignment has focused regression coverage across supported value types and lazy loading. No concrete merge-blocking risk remains.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main change: aligning database join keys with equality identity.
Description check ✅ Passed The description is complete and directly addresses the template requirements. It explains the changes and motivation, documents extensive verification, and confirms the changeset and published-code im…
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/wave1-query-equality

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Sep 16, 2026

Copy link
Copy Markdown
More templates

@tanstack/angular-db

npm i https://pkg.pr.new/@tanstack/angular-db@1834

@tanstack/browser-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/browser-db-sqlite-persistence@1834

@tanstack/capacitor-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/capacitor-db-sqlite-persistence@1834

@tanstack/cloudflare-durable-objects-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/cloudflare-durable-objects-db-sqlite-persistence@1834

@tanstack/db

npm i https://pkg.pr.new/@tanstack/db@1834

@tanstack/db-ivm

npm i https://pkg.pr.new/@tanstack/db-ivm@1834

@tanstack/db-sqlite-persistence-core

npm i https://pkg.pr.new/@tanstack/db-sqlite-persistence-core@1834

@tanstack/electric-db-collection

npm i https://pkg.pr.new/@tanstack/electric-db-collection@1834

@tanstack/electron-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/electron-db-sqlite-persistence@1834

@tanstack/expo-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/expo-db-sqlite-persistence@1834

@tanstack/node-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/node-db-sqlite-persistence@1834

@tanstack/offline-transactions

npm i https://pkg.pr.new/@tanstack/offline-transactions@1834

@tanstack/powersync-db-collection

npm i https://pkg.pr.new/@tanstack/powersync-db-collection@1834

@tanstack/query-db-collection

npm i https://pkg.pr.new/@tanstack/query-db-collection@1834

@tanstack/react-db

npm i https://pkg.pr.new/@tanstack/react-db@1834

@tanstack/react-native-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/react-native-db-sqlite-persistence@1834

@tanstack/react-router-with-db

npm i https://pkg.pr.new/@tanstack/react-router-with-db@1834

@tanstack/rxdb-db-collection

npm i https://pkg.pr.new/@tanstack/rxdb-db-collection@1834

@tanstack/solid-db

npm i https://pkg.pr.new/@tanstack/solid-db@1834

@tanstack/svelte-db

npm i https://pkg.pr.new/@tanstack/svelte-db@1834

@tanstack/tauri-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/tauri-db-sqlite-persistence@1834

@tanstack/trailbase-db-collection

npm i https://pkg.pr.new/@tanstack/trailbase-db-collection@1834

@tanstack/vue-db

npm i https://pkg.pr.new/@tanstack/vue-db@1834

commit: 5111a82

@github-actions

github-actions Bot commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Size Change: -51 B (-0.03%)

Total Size: 165 kB

📦 View Changed
Filename Size Change
packages/db/dist/esm/query/compiler/joins.js 2.95 kB -51 B (-1.7%)
ℹ️ View Unchanged
Filename Size
packages/db/dist/esm/client.js 3.66 kB
packages/db/dist/esm/collection-options.js 236 B
packages/db/dist/esm/collection/change-events.js 1.44 kB
packages/db/dist/esm/collection/changes.js 2.23 kB
packages/db/dist/esm/collection/cleanup-queue.js 794 B
packages/db/dist/esm/collection/events.js 481 B
packages/db/dist/esm/collection/index.js 4.58 kB
packages/db/dist/esm/collection/indexes.js 1.99 kB
packages/db/dist/esm/collection/lifecycle.js 2.15 kB
packages/db/dist/esm/collection/mutations.js 2.53 kB
packages/db/dist/esm/collection/state.js 6.44 kB
packages/db/dist/esm/collection/subscription.js 8.72 kB
packages/db/dist/esm/collection/sync.js 4.62 kB
packages/db/dist/esm/collection/transaction-metadata.js 144 B
packages/db/dist/esm/deferred.js 207 B
packages/db/dist/esm/errors.js 5.26 kB
packages/db/dist/esm/event-emitter.js 964 B
packages/db/dist/esm/index.js 3.68 kB
packages/db/dist/esm/indexes/auto-index.js 829 B
packages/db/dist/esm/indexes/base-index.js 1.14 kB
packages/db/dist/esm/indexes/basic-index.js 2.07 kB
packages/db/dist/esm/indexes/btree-index.js 2.26 kB
packages/db/dist/esm/indexes/index-registry.js 820 B
packages/db/dist/esm/indexes/reverse-index.js 376 B
packages/db/dist/esm/live-query-adapter.js 318 B
packages/db/dist/esm/live-query-observer.js 3.69 kB
packages/db/dist/esm/live-query-options.js 702 B
packages/db/dist/esm/live-query-window-controller.js 4.36 kB
packages/db/dist/esm/local-only.js 975 B
packages/db/dist/esm/local-storage.js 2.15 kB
packages/db/dist/esm/optimistic-action.js 359 B
packages/db/dist/esm/paced-mutations.js 496 B
packages/db/dist/esm/proxy.js 3.32 kB
packages/db/dist/esm/query/builder/functions.js 1.47 kB
packages/db/dist/esm/query/builder/index.js 6.69 kB
packages/db/dist/esm/query/builder/query-ir.js 116 B
packages/db/dist/esm/query/builder/ref-proxy.js 1.24 kB
packages/db/dist/esm/query/compiler/evaluators.js 1.92 kB
packages/db/dist/esm/query/compiler/expressions.js 560 B
packages/db/dist/esm/query/compiler/group-by.js 4.13 kB
packages/db/dist/esm/query/compiler/index.js 9.06 kB
packages/db/dist/esm/query/compiler/lazy-targets.js 1.1 kB
packages/db/dist/esm/query/compiler/order-by.js 1.91 kB
packages/db/dist/esm/query/compiler/parent-routes.js 319 B
packages/db/dist/esm/query/compiler/route-metadata.js 1.24 kB
packages/db/dist/esm/query/compiler/select.js 1.58 kB
packages/db/dist/esm/query/effect.js 4.6 kB
packages/db/dist/esm/query/equality-value-identity.js 591 B
packages/db/dist/esm/query/expression-helpers.js 1.43 kB
packages/db/dist/esm/query/ir-stable-identity.js 4.04 kB
packages/db/dist/esm/query/ir.js 1.59 kB
packages/db/dist/esm/query/live-query-collection.js 391 B
packages/db/dist/esm/query/live/bucket-facade-adapter.js 2.73 kB
packages/db/dist/esm/query/live/collection-config-builder.js 6.97 kB
packages/db/dist/esm/query/live/collection-registry.js 264 B
packages/db/dist/esm/query/live/collection-subscriber.js 2.25 kB
packages/db/dist/esm/query/live/internal.js 145 B
packages/db/dist/esm/query/live/materialized-pipeline.js 2.32 kB
packages/db/dist/esm/query/live/ordered-source-loader.js 3.14 kB
packages/db/dist/esm/query/live/subset-demand-controller.js 1.26 kB
packages/db/dist/esm/query/live/utils.js 1.14 kB
packages/db/dist/esm/query/optimizer.js 2.91 kB
packages/db/dist/esm/query/query-once.js 359 B
packages/db/dist/esm/query/runtime-reference-identity.js 572 B
packages/db/dist/esm/query/subset-dedupe.js 486 B
packages/db/dist/esm/scheduler.js 1.34 kB
packages/db/dist/esm/SortedMap.js 1.3 kB
packages/db/dist/esm/strategies/debounceStrategy.js 247 B
packages/db/dist/esm/strategies/queueStrategy.js 428 B
packages/db/dist/esm/strategies/throttleStrategy.js 246 B
packages/db/dist/esm/transactions.js 3.51 kB
packages/db/dist/esm/utils.js 1.01 kB
packages/db/dist/esm/utils/array-utils.js 270 B
packages/db/dist/esm/utils/browser-polyfills.js 304 B
packages/db/dist/esm/utils/btree.js 4.51 kB
packages/db/dist/esm/utils/callbacks.js 174 B
packages/db/dist/esm/utils/comparison.js 1.49 kB
packages/db/dist/esm/utils/cursor.js 676 B
packages/db/dist/esm/utils/error.js 167 B
packages/db/dist/esm/utils/get-or-create.js 155 B
packages/db/dist/esm/utils/index-optimization.js 2.42 kB
packages/db/dist/esm/utils/type-guards.js 230 B
packages/db/dist/esm/utils/uuid.js 449 B
packages/db/dist/esm/virtual-props.js 360 B

compressed-size-action::db-package-size

@github-actions

Copy link
Copy Markdown
Contributor

Size Change: 0 B

Total Size: 7.34 kB

ℹ️ View Unchanged
Filename Size
packages/react-db/dist/esm/DbProvider.js 317 B
packages/react-db/dist/esm/HydrationBoundary.js 263 B
packages/react-db/dist/esm/index.js 330 B
packages/react-db/dist/esm/live-query-internals.js 282 B
packages/react-db/dist/esm/useLiveInfiniteQuery.js 1.9 kB
packages/react-db/dist/esm/useLiveQuery.js 2.68 kB
packages/react-db/dist/esm/useLiveQueryEffect.js 355 B
packages/react-db/dist/esm/useLiveSuspenseQuery.js 812 B
packages/react-db/dist/esm/usePacedMutations.js 401 B

compressed-size-action::react-db-package-size

@KyleAMathews
KyleAMathews merged commit 1e54c6a into main Sep 17, 2026
11 checks passed
@KyleAMathews
KyleAMathews deleted the codex/wave1-query-equality branch September 17, 2026 12:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant